「退款成功」這四個字,在前端、後端、金流商與客戶那裡,是四件不同的事;做到一半才發現,沒有一件被定義過。
昨天整合日的火勉強撲掉了。欄位語意對過一輪,線接通了,在測試環境按下「退款」,畫面會亮出綠色的「退款成功」。看板重新變綠,群組裡有人貼了慶祝訊息。
聯調第三天,年輕的後端工程師盯著金流回應的 log,隨口問了一句:
「所以金流回 200,這筆就是真的退成功了嗎?」
負責金流串接的工程師頭也沒抬:「200 只是受理。實際成功還是失敗,要等金流商的 webhook 通知,或是自己拿單號去查。」
他說完,才發現會議室安靜下來了。
前端那個秒亮的「退款成功」畫面。後端那行「收到 200 就把訂單改成已退款」的程式。兩個「完成」,全部蓋在同一個沒有人驗證過的假設上:200 等於退款成功。
專案做到一半,Ticket 開了三週,程式寫了幾千行,這是第一次有人問出這個問題:
怎樣叫退款成功?
事後回看,每個人心裡其實都有一個「成功」的定義,而且單獨看都有道理。
前端工程師想的是:API 回成功我就顯示成功、回失敗我就顯示失敗,每個功能都是這樣做的,天經地義。
後端工程師想的是:呼叫金流 API 拿到 HTTP 200 就是呼叫成功,呼叫成功就更新訂單狀態,教科書不都這樣寫。
金流串接工程師想的是:文件寫得清清楚楚,200 是受理,結果要等通知——這是常識吧,需要特別講嗎?
PM 想的是:三張 Ticket 都拖進 Done 了,完成就是完成。
四個人、四個「成功」,各自在自己那一層都沒有錯。問題從來不是誰理解錯了,而是沒有人發現「退款成功」在這個房間裡指的根本不是同一件事——而且整整三週,沒有任何一個環節逼大家把它攤開來對一次。
把這個動作解剖開來,「退款成功」至少疊了五層:
HTTP 200
≠ refund accepted(金流商受理這筆退款申請)
≠ payment reversed(錢真的被退回)
≠ state confirmed(我們的系統確認並更新狀態)
≠ user goal achieved(客戶拿回錢,而且知道拿回了)
一層一層拆。
第一層,HTTP 200。這是通訊層的回條:請求送到了、對方有回話。僅此而已,它連「對方看懂了你的請求」都不保證——很多服務照樣用 200 包著錯誤代碼回你。確認方式:讀回應本身。
第二層,refund accepted。在本案金流商的約定裡,200 搭配回應裡的結果代碼,代表「受理」:申請被收下、排入處理。受理與退到錢的距離,大概等於投出履歷與拿到聘書的距離。而且從這一層開始,主導權就不在我們系統手上了。
第三層,payment reversed。錢真的動了:款項被退回持卡人那一側。這件事發生在別人的系統、別人的時間表裡,誰來告訴你?兩條路——金流商的 webhook 主動通知,或我們拿單號主動查詢。而 webhook,昨天才盤點過,三方都以為不歸自己,沒有人串。也就是說,這一層在我們系統裡目前是全盲的。金流串接工程師順帶補了一句:webhook 偶爾會重送,同一筆通知可能來兩次。沒有人接話,會議記錄上也沒有這一行。
第四層,state confirmed。金流商說退了,我們的訂單狀態要跟著變,而且要跟金流商的紀錄對得起來——財務對帳,對的就是這兩邊。金流商日結,對帳檔隔天才有,帳面上的最終確認天生慢一天。而我們的後端在收到 200 的那一秒,就把訂單改成「已退款」:拿第一層的回條,蓋了第四層的章。
第五層,user goal achieved。客戶按下退款,他要的不是一個綠色勾勾,是錢回到帳上、而且他知道回來了。信用卡退款從受理到出現在帳單上,往往要好幾個工作天。前端在第零秒顯示「退款成功」,等於替金流商簽了一張它從來沒答應過的支票。
每一層的「成功」,都要回答同樣的兩個問題:這一層成功是什麼意思?由誰、用什麼機制確認?
這個專案在動工前回答了幾層?零層。動工三週後呢?半層——我們現在知道 200 是受理了,但受理之後的每一層,仍然沒有人負責確認。
那麼,從受理到確認之間,畫面該顯示什麼?
三個字:處理中。
很多工程師不喜歡這三個字,覺得像是系統做不到即時、像是自己的失敗。剛好相反——錢正在別人的系統裡移動、結果還沒回來,「處理中」是這個當下唯一誠實的答案。還不知道結果就說還不知道,這不叫體驗差,這叫不說謊。
問題是前端的畫面只有兩格:成功、失敗。昨天說過,前端等的是一個 boolean。二態的畫面裝不下三態的世界,多出來的那一態,就被四捨五入成「成功」。
這也不是什麼新問題。瀑布的做法:設計階段的介面規格就要寫明「此 API 為非同步、結果用什麼機制確認、確認前狀態為何」,驗收依據跟著走。敏捷的做法:這條 Story 的 Acceptance Criteria 本來就該寫出可觀察的完成條件——按下退款之後,使用者在哪個時點看到什麼(Day 10 說過,AC 不是文件格式,是把「怎樣算好」變成共識)。兩邊都沒有一條規則叫做「回 200 就算成功」。
缺的從來不是方法論。缺的是動工之前,有人把今天標題那個問題問出來。
把時間倒回第二部的世界,這件事大概根本不會成為一件事。
如果資深工程師在場,接金流的第一天他就會問:「退款是同步還是非同步?」——被非同步咬過的人,看到「金流」兩個字就會先問這一題。然後他會順手在白板上畫出狀態:申請、處理中、成功、失敗;會提醒 webhook 要列進工作項;會記得對帳檔隔天才有。專案會順順地往前走,順到沒有人發現這裡曾經有一個坑。
這次他被借調去救另一個專案,不在。
於是暴露出一個很有意思的事實:知識其實一直都在場。金流商的文件白紙黑字寫著 200 是受理;金流串接工程師讀過,也一直知道。他沒有講,因為沒有人問,而他以為這是常識——就像每個人都以為 webhook 有人串。
所以大神真正的功能,從來不只是知道答案,而是會在對的時間把對的問題問出來。Day 15 的開工前五問裡,有一格就叫「怎樣算成功」。那一格不需要天分才填得出來,它需要的只是流程裡有一個位置,逼你在動工之前填掉它。
三週前沒有人填。今天,整個團隊用一場鴉雀無聲的聯調會議,把它補填完畢。
盤點帳單。那個秒亮的「退款成功」畫面,已經在測試環境跑了兩週、展示過、被稱讚過;現在後端的狀態要重想、前端的畫面要重排、沒人認領的 webhook 要補——這些工,原本不在任何人的估算裡。
Scope □ 一項都沒有少
Time □ 上線日還是沒有人敢動
Cost □ 沒有加人
Quality ■ 蓋在錯誤完成語意上的畫面與狀態,跑了兩週
Risk □ 沒有人重新評估還有幾層沒定義
人 ■ 聯調加班、重寫狀態、補做沒人認領的工 ← 大神不在,還是這格
Day 15 說過,這個案子的風險是一顆一顆堆上去的未爆彈。今天爆的這顆叫完成語意,拆彈方式一如既往:品質先賠,人再加班。
完成語意晚一天定義,就有人拿著錯的定義多施工一天;今天多蓋上去的每一層,都是明天要拆的。
任何跨系統的動作——退款、付款、寄通知、開發票、同步資料——動工前先把這張表填完:
□ 這個動作從發起到結束,一共有幾層「完成」?
(例:送出 → 受理 → 實際生效 → 狀態確認 → 使用者目標)
□ 每一層的「成功」是什麼意思?
□ 每一層由誰、或用什麼機制確認?
(回應代碼/非同步通知/主動查詢/對帳檔:__)
□ 最後一層確認之前與之後,使用者各看到什麼?
最後一格填不出來,你的畫面上遲早會出現一個「猜的成功」;填得出來,你才有資格決定那個綠色勾勾該在哪一秒亮起。
系統回你 200,意思是「我聽到了」;把「我聽到了」翻譯成「我做完了」,是這個專案最貴的一次翻譯。
那天下午,前端把畫面改成三個狀態:處理中、成功、失敗。大家看著新畫面,覺得這個功能終於誠實了。有人提議:拿給客服主管看看吧,順便展示一下進度。
她盯著「處理中」三個字看了幾秒,說出的第一句話是:
「不是這樣。超過七天的,要先給財務簽核。」
會議室再度安靜。等等——這句話,算需求變更嗎?明天來把這件事分清楚。